# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""

Classes:
Config -- Ye Olde Configuration

Functions:
Get -- Return a new Config object, loaded from configdir and /etc (if system)

"""


import ConfigParser
import os
import re
import socket

from errno import ENOENT

import svn.core
from svn.core import SVN_CONFIG_OPTION_STORE_PASSWORDS
from svn.core import SVN_CONFIG_OPTION_STORE_AUTH_CREDS
from svn.core import SVN_CONFIG_SECTION_AUTH
from svn.core import svn_config_get_bool, svn_config_merge, svn_config_read
from svn.core import svn_config_get_config

import gvn.errors
import gvn.platform
import gvn.util


PROJECT_FILENAME_PAT = '^[A-Za-z0-9][A-Za-z0-9_-]*$'
PROJECT_FILENAME_RE = re.compile(PROJECT_FILENAME_PAT)


# These only exist for ease of testing.
_GetDefaultUser = gvn.platform.GetUserName
_GetDefaultSvnUserConfigDir = gvn.platform.GetSvnUserConfigDir
_GetDefaultSvnSystemConfigDir = gvn.platform.GetSvnSystemConfigDir
_GetDefaultGvnUserConf = gvn.platform.GetGvnUserConf
_GetDefaultGvnSystemConf = gvn.platform.GetGvnSystemConf

def _GetDomainName():
  return '.'.join(socket.getfqdn().split('.')[-2:])

_DEFAULT_STORE_AUTH_CREDS = True
_DEFAULT_STORE_PASSWORDS = True


class BaseConfig(object):
  def __init__(self):
    self._data = {}

  def _GetBool(self, name):
    return self._data[name]
  def _SetBool(self, name, value):
    self._data[name] = value
  _GetStr = _GetBool
  _SetStr = _SetBool


class ProjectConfig(BaseConfig):
  def __init__(self, username=None, url=None, email_domain=None):
    BaseConfig.__init__(self)
    self.username = username
    self.URL = url
    self.email_domain = email_domain

  URL = property(lambda s: s._GetStr('URL'),
                 lambda s, val: s._SetStr('URL', val))
  username = property(lambda s: s._GetStr('Username'),
                      lambda s, val: s._SetStr('Username', val))

  def _GetEmailAddress(self):
    try:
      if self._data['email_address'] is not None:
        return self._data['email_address']
    except KeyError:
      pass
    try:
      address = os.environ['EMAIL'].strip()
      if len(address) > 0:
        return address
    except KeyError:
      pass
    if '@' in self.username:
      return self.username
    return '@'.join([self.username, _GetDomainName()])
  email_address = property(_GetEmailAddress,
                           lambda s, val: s._SetStr('email_address', val))
  email_domain = property(lambda s: s._GetStr('email_domain'),
                          lambda s, val: s._SetStr('email_domain', val))


class Config(BaseConfig):
  def __init__(self):
    BaseConfig.__init__(self)

    self._data = {
        'default_username': None,
        'svn_config_hash': None,
        'svn_config_dir': None,
        'store_auth_creds': None,
        'store_passwords': None,
        'smtp_server': None,
        'smtp_user': None,
        'smtp_password': None,
        'diff_command': None,
        'editor_command': None,
        'encoding': None,
        }

    self.project_dirs = []

  # TODO(epg): Get store_auth_creds and store_passwords from svn_config.
  # It's bogus to parse the config files here after svn has already
  # done it, but we can't seem to get the svn_config_t out of the
  # apr_hash_t svn_config_get_config returns.
  store_auth_creds = property(lambda s: s._GetBool('store_auth_creds'),
                            lambda s, val: s._SetBool('store_auth_creds', val),
   doc="""Whether auth info other than passwords are cached

   e.g. ssl certificate trust""")
  store_passwords = property(lambda s: s._GetBool('store_passwords'),
                            lambda s, val: s._SetBool('store_passwords', val),
                            doc="""Whether passwords are cached""")

  def SetSvnConfig(self, value, pool):
    self._data['svn_config_hash'] = svn_config_get_config(value.encode('utf-8'),
                                                          pool)
    self._data['svn_config_dir'] = value

    # See comment above store_auth_creds above; this is bogus.
    self.svn_config = svn_config_read(os.path.join(_GetDefaultSvnSystemConfigDir(),
                                              'config'),
                              False,    # must_exist
                              pool)
    svn_config_merge(self.svn_config,
                     os.path.join(self._data['svn_config_dir'],
                                  'config').encode('utf-8'),
                     False)             # must_exist

    self.store_auth_creds = svn_config_get_bool(self.svn_config,
                                              SVN_CONFIG_SECTION_AUTH,
                                            SVN_CONFIG_OPTION_STORE_AUTH_CREDS,
                                              _DEFAULT_STORE_AUTH_CREDS)
    self.store_passwords = svn_config_get_bool(self.svn_config,
                                              SVN_CONFIG_SECTION_AUTH,
                                              SVN_CONFIG_OPTION_STORE_PASSWORDS,
                                              _DEFAULT_STORE_PASSWORDS)

  svn_config_hash = property(lambda s: s._GetStr('svn_config_hash'))
  svn_config_dir = property(lambda s: s._GetStr('svn_config_dir'))

  smtp_server = property(lambda s: s._GetStr('smtp_server'),
                        lambda s, val: s._SetStr('smtp_server', val))
  smtp_user = property(lambda s: s._GetStr('smtp_user'),
                       lambda s, val: s._SetStr('smtp_user', val))
  smtp_password = property(lambda s: s._GetStr('smtp_password'),
                           lambda s, val: s._SetStr('smtp_password', val))
  diff_command = property(lambda s: s._GetStr('diff_command'),
                         lambda s, val: s._SetStr('diff_command', val))

  def _GetSvnConfigOption(self, option, svn_section, svn_option, default=None):
    """Return the value for option from run-time, svn, or default value.

    If set at run-time (e.g. via environment variable or command-line
    option) return that value, else if set in svn config return that,
    else return default.

    Arguments:
    option      -- name of the option
    svn_section -- name of the section in svn config
    svn_option  -- name of the option in svn config
    default     -- default if not set from svn config or at run-time
    """
    result = self._GetStr(option)
    if result is None:
      result = svn.core.svn_config_get(self.svn_config,
                                       svn_section, svn_option, default)
      self._SetStr(option, result)
    return result

  editor_command = property(lambda self: self._GetSvnConfigOption(
                                         'editor_command',
                                         svn.core.SVN_CONFIG_SECTION_HELPERS,
                                         svn.core.SVN_CONFIG_OPTION_EDITOR_CMD,
                                         gvn.platform.DEFAULT_EDITOR_COMMAND),
                            lambda s, val: s._SetStr('editor_command', val))
  encoding = property(lambda self: self._GetSvnConfigOption('encoding',
                                       svn.core.SVN_CONFIG_SECTION_MISCELLANY,
                                       svn.core.SVN_CONFIG_OPTION_LOG_ENCODING,
                                       gvn.platform.DefaultEncoding()),
                      lambda s, val: s._SetStr('encoding', val))

  default_username = property(lambda s: s._GetStr('default_username'))

  # XXX not sure about any of this config stuff, but least sure about
  # these project parts
  # XXX(pamg): Not positive about these project parts either, on Windows.

  def ProjectFile(self, name, mode='r'):
    """Return a file object for project name; mode is same as for file.

    Raises gvn.errors.NoProject if no project by this name.

    """

    for i in self.project_dirs:
      try:
        return open(os.path.join(i, name), mode)
      except IOError, e:
        if e.errno != ENOENT:
          raise
    raise gvn.errors.NoProject(name)

  def ProjectByName(self, name):
    """Return (username, url) for the first project matching name.

    Raises gvn.errors.InvalidProjectName if does not match the
    filename constraining pattern.

    Raises gvn.errors.NoProject if no project by this name.

    """

    if not PROJECT_FILENAME_RE.match(name):
      raise gvn.errors.InvalidProjectName(name)

    result = ProjectConfig()
    parser = ConfigParser.RawConfigParser()
    parser.readfp(self.ProjectFile(name))
    try:
      result.username = parser.get('project', 'username')
    except ConfigParser.NoOptionError:
      result.username = self.default_username
    try:
      result.email_address = parser.get('project', 'email_address')
    except ConfigParser.NoOptionError:
      result.email_address = None
    try:
      result.email_domain = parser.get('project', 'email_domain')
    except ConfigParser.NoOptionError:
      result.email_domain = None
    # URL is required, though.
    result.URL = parser.get('project', 'URL').rstrip('/')
    return result

  def ProjectByURL(self, url, testing=False):
    """Return Project whose URL has the longest match for url.

    Raises gvn.errors.NoProject if no project matches.

    """

    names = set()
    for i in self.project_dirs:
      try:
        for name in os.listdir(i):
          names.add(name)
      except OSError, e:
        if e.errno not in gvn.platform.ENOENT_codes:
          raise

    if testing:
      names = sorted(names)

    found = None
    for name in names:
      try:
        project = self.ProjectByName(name)
      except gvn.errors.InvalidProjectName, e:
        # Ignore invalid filenames and move on.
        continue

      if gvn.util.IsChild(url, project.URL):
        if found is None or len(project.URL) > len(found.URL):
          # Save this as the winning match if it matches longer than
          # the last match or there is no last match yet.
          found = project

    if found is None:
      raise gvn.errors.NoProject(url)
    return found

  def ProjectDefault(self):
    """Return ProjectConfig instance for default project.

    Raises gvn.errors.NoProject if no default project.

    """

    return self.ProjectByName('default')


def _SetDefaults(config):
  config._data['default_username'] = _GetDefaultUser()
  config._data['svn_config_dir'] = _GetDefaultSvnSystemConfigDir()
  config.smtp_server = 'smtp'
  config.diff_command = 'internal'


def _Load(config, configdir):
  parser = ConfigParser.RawConfigParser()
  fn = os.path.join(configdir, 'config')
  if parser.read(fn) == [fn]:
    # These settings are optional and should therefore not raise an Error.
    for option in ['smtp_server', 'smtp_user', 'smtp_password',
                   'diff_command']:
      try:
        setattr(config, option, parser.get('external', option))
      except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
        pass

  config.project_dirs.insert(0, os.path.join(configdir, 'projects'))

def Get(configdir=None, svn_configdir=None, system=True, pool=None):
  """Return a new Config object, loaded from configdir and /etc (if system).

  Arguments:
  configdir     -- path to a gvn config directory (defaults to ~/.gvn)
  svn_configdir -- path to a subversion config directory
                   (defaults to ~/.subversion)
  system        -- whether to load system config (/etc/gvn)
  pool          -- memory pool

  """

  config = Config()
  _SetDefaults(config)

  if system:
    _Load(config, _GetDefaultGvnSystemConf())

  if configdir is None:
    configdir = _GetDefaultGvnUserConf()
  _Load(config, configdir)

  if svn_configdir is None:
    svn_configdir = _GetDefaultSvnUserConfigDir()
  config.SetSvnConfig(svn_configdir, pool)

  # TODO(epg): Belongs in cmdline.  Also --diff-cmd and GVNDIFF or
  # something like that.
  for i in 'GVN_EDITOR', 'SVN_EDITOR', 'VISUAL', 'EDITOR':
    try:
      config.editor_command = os.environ[i]
      break
    except KeyError:
      pass

  return config
